推荐一篇文章,《Java垃圾回收机制》(简书作者:可文分身),深入浅出。再结合比如这篇IBM的《Java内存详解》,以及Oracle的Java使用手册,对Java的垃圾回收机制先有了一个初步的了解。感谢原作者可文分身以及Andrew Hall。
首先从机器层面讲,如上图所示操作系统和C-运行时库肯定本身就占一部分内存。如果我们把系统分配给Java进程的所有内存统称为Java Runtime Area的话,系统可以笼统地分成三块,1)系统区(灰色) 2)Java区(蓝色的逻辑堆和绿色的JVM) 3)剩下的本机堆(淡绿色)。上图中吧Java Runtime Area分成蓝色的逻辑堆和绿色的JVM区实际上并不准确。那么一个Java进程到底包含哪些信息,占用哪些内存空间呢?我们先看下面这这两张图:
根据上图,一个Java程序所占用的空间主要可以分成三大块。如果搞清楚了一个编译过的代码到底是怎么被执行的,那么内存的分配其实很好理解:
下面这张图图就是对Java Runtime Data的各部分分区的很好的总结,基本一一对应了我上面提到的重要的点。Oracle的Java使用手册 第二章-虚拟机的结构里对图中的每一部分都有准确的描述,不明白可以去查。
现在我们把灯光对准今天的主角。如下图所示,逻辑堆分成“年轻代”和“老年代”两部分。图中的永生代请无视,理由上面解释过了。而年轻代又分为两种,一种是“Eden”区域(伊甸园,名字好美),另外一种是两个大小对等的Survivor区域:from区和to区。这名字其实很形象,因为一个新实例化的对象,它的内存分配都在年轻代,具体地说是在年轻代的Eden区,小孩都在伊甸园光着屁股跑。而老年代的实例年龄就要大很多,而且比年轻代的实例更稳定。之所以将Java内存按照分代进行组织,主要是基于这样一个事实:大多数对象都在年轻时候死亡。所以年轻代相对老年代需要更频繁的清理。把他们区分开来,配用不同的清理策略,有助于提高效率。
在年轻代上,Java的垃圾回收使用的是Mark-Copy算法。顾名思义,算法分成Mark和Copy两个步骤。Mark指的是标记出所有还活着的实例,然后清扫掉所有未被标记的实例,空出内存,实际这个过程叫做Mark-Sweep算法(详见“标记-清扫算法”这篇文章)。然后Copy部分就是将幸存的不同年龄的实例拷贝到别的分代。下面我们就对这两个过程一一讲解。
讲到垃圾回收,我们的第一反应一定是怎么标记垃圾。比如最简单的区分技术:引用计数(reference conunting)。每个对象都含有一个引用计数器。当有引用指向对象时,计数器加一。引用脱钩,计数器减一。但这个方法有个缺陷,想想看两个对象互相引用,但实际上他们已经脱离全世界的情况,他们各自的计数器都不是零。所以这种方法几乎很少被使用。
Java垃圾回收使用的策略恰好相反,是标记所有存活的实例,其他的全部清除。考虑到”大多数对象都在年轻时死亡”这个事实,搜索活着的比搜索死去的更省事儿。从下图中,我们可以看到Java是从所谓的“根对象”开始地毯式扫描,遍历所有和根对象有直接或间接引用关系的实例。
那关键问题是,哪些对象是“根对象”呢?根据“How Garbage Really Works”这篇文章,根对象主要包括四类对象:
java.lang.ClassLoader
类加载器加载,类的数据都不在逻辑堆,而是存在永生代,也就是Method Area方法区,现在叫Metaspace。类本身一旦被GC清除,他的所有静态变量也就跟着被释放了。所以我们总结一下Mark-Sweep算法(转自“标记-清扫算法(简书作者:可文分身)”):
在标记阶段,mutator先中断整个程序的运行(Stop-The-World的称呼由此而来)。然后collector从根对象开始进行遍历,对从mutator根对象可以访问到的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象。然后清除阶段,collector对堆内存(heap memory)从头到尾进行线性的遍历,如果发现某个对象没有标记为可达对象-通过读取对象的header信息,则就将其回收。在清除完成以后,mutator在回复程序的运行。
如下图1所示,新对象的内存分配先分配在Eden区域,当Eden区域的空间不足于分配新对象时,就会触发年轻代上的垃圾回收(发生在Eden和Survivor内存区域上),我们称之为“Minor Garbage Collection”。 同时,每个对象都有一个“年龄”,这个年龄实际上指的就是该对象经历过的minor gc的次数。如图1所示,当对象刚分配到Eden区域时,对象的年龄为“0”,当minor gc被触发后,所有存活的对象(根据前面的Mark-Sweep算法)会被拷贝到其中一个Survivor区域,同时年龄增长为“1”。并清除整个Eden内存区域中的非可达对象。
当第二次minor gc被触发时(如图2所示),JVM再次通过Mark算法找出所有在Eden内存区域和Survivor1内存区域存活的对象,并将他们拷贝到新的Survivor2内存区域(这也就是为什么需要两个大小一样的Survivor区域的原因,两个区被交替使用,确保其中一个全空),同时对象的年龄加1. 最后,清除所有在Eden内存区域和Survivor1内存区域的非可达对象。
当对象的年龄足够大(这个年龄可以通过JVM参数进行指定,这里假定是2),当minor gc再次发生时,它会从Survivor内存区域中升级到年老代中,如图3所示。其实,即使对象的年龄不够大,但是Survivor内存区域中没有足够的空间来容纳从Eden升级过来的对象时,也会有部分对象直接升级到Tenured内存区域中。
当minor gc发生时,又有对象从Survivor区域升级到Tenured区域,但是Tenured区域已经没有空间容纳新的对象了,那么这个时候就会触发年老代上的垃圾回收,我们称之为“Major Garbage Collection”. 而在年老代上选择的垃圾回收算法则取决于JVM上采用的是什么垃圾回收器。通过的垃圾回收器有两种:Parallel Scavenge(PS) 和Concurrent Mark Sweep(CMS)。他们主要的不同体现在年老代的垃圾回收过程中,年轻代的垃圾回收过程他们都使用前文分析的Mark-Copy算法。顾名思义,Parallel Scavenge垃圾回收器在执行垃圾回收时使用了多线程,以提高垃圾回收的效率。而Concurrent Mark Sweep回收器主要是应用程序挂起”Stop The World”的时间比较短,更接近并发。
和Mark-Copy算法不同,PS算法在执行的是Mark-Compact过程。Mark还是之前的mark-sweep过程,标记存活实例,清除不可达实例。不同的是没有一个预留的survivor区来全部拷贝过去。主要是考虑到老年代比较稳定,也比较大,全部拷贝效率上划不来。但问题是空间会碎片化,以后大一点的对象存不进来。所以要来一个compact碎片整理。
前面讲了,CMS主要特点是并发,Stop-The-World时间短。从他的名字可以看出,他的主要思想还是源于Mark-Sweep。下面看看他的并发具体是怎么实现的。
所以实际上CMS就是节省了从跟对象一代子对象往下搜索全部可达对象的时间。但CMS有个明显的缺点,就是他没有碎片整理的过程。对空间的利用不好,容易引发out of memory。
针对CMS这个没有碎片整理的问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器 - Garbage First(G1)垃圾收集器。
G1垃圾收集器和CMS垃圾收集器有几点不同。首先,最大的不同是内存的组织方式变了。Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了下图中一个个大小一样的Region - 每个region从1M到32M不等。
一个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过一个region大小的50%的对象。区隔变小的好处这里就体现出来了,对这种Humongous区就能特殊情况特殊照顾了,省了很多扫描的时间。
在G1垃圾收集器中,年轻代的垃圾回收过程跟PS垃圾收集器和CMS垃圾收集器差不多,新对象的分配还是在Eden region中,当所有Eden region的大小超过某个值时,触发minor gc,回收Eden region和Survivor region上的非可达对象,同时升级存活的可达对象到对应的Survivor region和Tenured region上。对象从Survivor region升级到Tenured region依然是取决于对象的年龄。
对于年老代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样,但主要的改进有一下几项:
所以综合来讲,G1加上了CMS没有的碎片整理功能,同时程序挂起时间更短了,并发性更高了,而且存活对象的标记效率也更高了。目前G1正在全面替换掉CMS。
至此,我对Java垃圾回收器GC的原理做了一个简单的了解。我深深感受到系统过程设计的复杂度。所以当我们在愉快地编程的时候,JVM默默地为我们做了这么多的事。实际本文只是做一个草草的鸟览,文中所描述过程的真正复杂性还完全没有被揭示出来。事实上每一个对复杂性的掩盖,都凝聚着无数设计师深沉的思考。而这些深沉的思考才是我们人类进步最坚定有力的阶梯,至少比某些主义来的实用地多。我觉得它们很美。